﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Diagnostics.Contracts;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Newtonsoft.Json;
using Common.Logging;

using Common;
namespace WebServer.Handlers
{
    /// <summary>
    /// Indicates that the function is a rest function
    /// </summary>
    [AttributeUsage(AttributeTargets.Method)]
    public sealed class RestFunctionAttribute : Attribute
    {
        /// <summary>
        /// The method this function should respond to (Default GET)
        /// </summary>
        public HttpMethod Method { get; set; }
        /// <summary>
        /// The name this function exposes (Default Same as function name)
        /// </summary>
        public String Name { get; set; }
        /// <summary>
        /// Set to true if the results of this function are reliably the same in a given time period (Default False)
        /// </summary>
        public bool Cacheable { get; set; }
        /// <summary>
        /// The period of time for which a function with specific arguments can be assumed to return the same result
        /// </summary>
        public int ExpiryInMinutes { get; set; }
    }
    /// <summary>
    /// Indicates that the argument can be ommitted
    /// </summary>
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1019:DefineAccessorsForAttributeArguments"), AttributeUsage(AttributeTargets.Parameter)]
    public sealed class OptionalAttribute : Attribute
    {
        /// <summary>
        /// Constructs an optional paramaeter that pulls the default value from the specifed member at run time
        /// </summary>
        /// <param name="name">The name of a member which can be accessed at run time to retrieve the default value for this argument</param>
        public OptionalAttribute (String name)
        {
            RuntimeName = name;
        }
        /// <summary>
        /// Constructs an optional paramaeter that pulls the default value from the specifed member at run time
        /// </summary>
        /// <param name="provider">The type containing the member which contains the default value for this argument at run time</param>
        /// <param name="name">The name of a member which can be accessed at run time to retrieve the default value for this argument</param>
        public OptionalAttribute(Type provider, String name)
        {
            RuntimeType = provider;
            RuntimeName = name;
        }
        /// <summary>
        /// The name of a member which can be accessed at run time to retrieve the default value for this argument
        /// </summary>
        public String RuntimeName { get; set; }
        /// <summary>
        /// The type containing the member which contains the default value for this argument at run time
        /// </summary>
        public Type RuntimeType { get; set; }
        
        /// <summary>
        /// The default value for this argument (cached once calculated)
        /// </summary>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")]
        public Object Value
        {
            get 
            {
                if (_value == null)
                {
                    MemberInfo valueProvider = RuntimeType.FindMembers(MemberTypes.All, BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.NonPublic, Type.FilterNameIgnoreCase, RuntimeName).FirstOrDefault();
                    if (valueProvider is FieldInfo)
                    { _value = ((FieldInfo)valueProvider).GetValue(null); }
                    else if (valueProvider is PropertyInfo)
                    { _value = ((PropertyInfo)valueProvider).GetValue(null, null); }
                    else if (valueProvider is MethodInfo)
                    { _value = ((MethodInfo)valueProvider).Invoke(null, new object[] { }); }
                }
                return _value;
            }
        }
        private Object _value = null;
    }

    /// <summary>
    /// The type of HTTP methods
    /// </summary>
    public enum HttpMethod
    {
        /// <summary>
        /// GET
        /// </summary>
        GET,
        /// <summary>
        /// POST
        /// </summary>
        POST,
        /// <summary>
        /// PUT
        /// </summary>
        PUT,
        /// <summary>
        /// DELETE
        /// </summary>
        DELETE
    }

    /// <summary>
    /// A value and the time it was calculated
    /// </summary>
    /// <typeparam name="TValue">The type of the value</typeparam>
    internal class CachedResult<TValue>
    {
        public CachedResult(TValue value)
        {
            Value = value;
            TimeStamp = DateTime.Now;
        }

        public DateTime TimeStamp { get; set; }
        public TValue Value { get; set; }
    }

    /// <summary>
    /// A handler which responds to Restful requests
    /// </summary>
    public class RESTHandler : IHttpRequestHandler, IDisposable
    {
        private class QuerySnapshot
        {
            public MethodInfo mi { get; set; }
            public Object instance { get; set; }
            public Object[] Args { get; set; }
        }

        private static readonly ILog log = LogManager.GetCurrentClassLogger();

        private Common.HalfLifeDictionary<String, CachedResult<String>> _cachedResults;
        private Common.MostPopularObjectTracker<String, QuerySnapshot> _popularQueries;
        private System.Threading.Timer _popularQueryUpdater;

        /// <summary>
        /// Constructs a rest handler but does not add any methods
        /// </summary>
        public RESTHandler() 
        {
            init(); 
        }
        /// <summary>
        /// Constructs a rest handler and adds instance rest methods from the specified object
        /// </summary>
        /// <param name="instance">An instance of an object containing rest methods</param>
        public RESTHandler(Object instance)
        {
            init();
            AddMethods(instance); 
        }
        /// <summary>
        /// Constructs a rest handler and adds static rest methods from the specfied type
        /// </summary>
        /// <param name="type">A type containing static rest methods</param>
        public RESTHandler(Type type)
        {
            init();
            AddMethods(type); 
        }
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
        private void init()
        {
            _cachedResults = new Common.HalfLifeDictionary<String, CachedResult<String>>() { HalfLife = TimeSpan.FromSeconds(30) };
            _popularQueries = new MostPopularObjectTracker<string, QuerySnapshot>();
            _popularQueryUpdater = new System.Threading.Timer(this.RefreshPopularQueries, null, TimeSpan.Zero, TimeSpan.FromSeconds(30));
        }
       
        /// <summary>
        /// Adds instance rest methods to the rest handler
        /// </summary>
        /// <param name="instance">An instance of an object which contains rest methods</param>
        public void AddMethods(Object instance)
        {
            Contract.Requires(instance != null);
            AddMethods(instance.GetType(), instance); 
        }
        /// <summary>
        /// Adds static rest methods to the rest handler
        /// </summary>
        /// <param name="type">A type containing static rest methods</param>
        public void AddMethods(Type type)
        { AddMethods(type, null); }
        
        private void AddMethods(Type type, Object instance)
        {
            Contract.Requires(type != null);
            foreach (MethodInfo m in type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static))
            {
                RestFunctionAttribute att = m.GetCustomAttributes(typeof(RestFunctionAttribute), false).FirstOrDefault() as RestFunctionAttribute;
                if (att != null)
                {
                    AddMethod(att.Name ?? m.Name, m, instance, att.Method, att.Cacheable, att.ExpiryInMinutes);
                }
            }
        }
        /// <summary>
        /// Creates a methods that responds to a restful request
        /// </summary>
        /// <param name="name">The name of the function</param>
        /// <param name="methodinfo">The method to be invoked</param>
        /// <param name="instance">An optional instance for the method to be invoked on</param>
        /// <param name="cacheable">True if the function reliably returns the same value for the same arguments (default false)</param>
        /// <param name="expiryInMinutes">The length of time the method can be assumed to return the same value for the same arguments</param>
        /// <param name="method">The http method this function responds to (default GET)</param>
        public void AddMethod(String name, MethodInfo methodinfo, Object instance, HttpMethod method = HttpMethod.GET, bool cacheable = false, int expiryInMinutes = 30)
        {
            Contract.Requires(name != null);
            Contract.Requires(methodinfo != null);

            String uname = name.ToUpperInvariant();
            ParameterInfo[] args = methodinfo.GetParameters();

            Dictionary<String, IList<RestFunctionInfo>> functions;
            if (!subhandlers.TryGetValue(method, out functions))
            {
                functions = new Dictionary<string, IList<RestFunctionInfo>>();
                subhandlers.Add(method, functions);
            }
            IList<RestFunctionInfo> overloads;
            if (!functions.TryGetValue(uname, out overloads))
            {
                overloads = new List<RestFunctionInfo>();
                functions.Add(uname, overloads);
            }
            int optionalArgs = args.Count(arg => IsOptional(arg));
            int numPermutations = (int)Math.Pow(2, optionalArgs);

            for (int permutation = 0; permutation < numPermutations; permutation++)
            {
                TimeSpan expiryTime = TimeSpan.FromMinutes(expiryInMinutes);
                if (_cachedResults.HalfLife < expiryTime)
                {
                    _cachedResults.HalfLife = expiryTime;
                    int nintyPercent = (int)( expiryTime.TotalMilliseconds * .9);
                    _popularQueryUpdater.Change(nintyPercent, nintyPercent);
                }
                RestFunctionInfo functionInfo = new RestFunctionInfo()
                {
                    Instance = instance,
                    MethodInfo = methodinfo,
                    Cachable = cacheable,
                    Expiry = expiryTime
                };
                int optionalArgIndex = -1;
                for (int i = 0; i < args.Length; i++)
                {
                    ParameterInfo param = args[i];
                    if (IsOptional(param))
                    {
                        optionalArgIndex++;
                        if ((permutation & (int) Math.Pow(2, optionalArgIndex)) == 0)
                        { continue; }
                    }
                    String pname = param.Name.ToUpperInvariant();
                    functionInfo.Arguments.Add(pname, param.ParameterType);
                    functionInfo.ArgumentNames.Add(pname);
                }
                overloads.Add(functionInfo);
            }
        }


        private static bool IsOptional(ParameterInfo arg)
        { return arg.IsOptional || GetOptionalAttribute(arg) != null; }
        private static OptionalAttribute GetOptionalAttribute(ParameterInfo arg)
        { return (OptionalAttribute)arg.GetCustomAttributes(typeof(OptionalAttribute), true).FirstOrDefault(); }
        private static Object GetDefaultValue(ParameterInfo arg)
        {
            if (arg.IsOptional)
            { return arg.DefaultValue; }
            OptionalAttribute opt = GetOptionalAttribute(arg);
            if (opt != null && opt.RuntimeType == null)
            { opt.RuntimeType = arg.Member.DeclaringType; }
            return opt != null ? opt.Value : null;
        }

        private class RestFunctionInfo
        {
            public Object Instance { get; set; }
            public MethodInfo MethodInfo { get; set; }
            public Dictionary<String, Type> Arguments { get { return _arguments; } }
            private Dictionary<String, Type> _arguments = new Dictionary<string, Type>();
            public HashSet<String> ArgumentNames {get{return _argumentNames;}}
            private HashSet<String> _argumentNames = new HashSet<string>();
            public bool Cachable { get; set; }
            public TimeSpan Expiry { get; set; }

        }

        Dictionary<HttpMethod, Dictionary<String, IList<RestFunctionInfo>>> subhandlers = new Dictionary<HttpMethod, Dictionary<string, IList<RestFunctionInfo>>>();

        /// <summary>
        /// Handles GET requests
        /// </summary>
        /// <param name="request">The request</param>
        /// <param name="response">The response</param>
        /// <param name="user">The detected users identity</param>
        public void Get(System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response, System.Security.Principal.IPrincipal user)
        {
            Handle(HttpMethod.GET, request, response);
        }

        /// <summary>
        /// Handles POST requests
        /// </summary>
        /// <param name="request">The request</param>
        /// <param name="response">The response</param>
        /// <param name="user">The detected users identity</param>
        public void Post(System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response, System.Security.Principal.IPrincipal user)
        {
            Handle(HttpMethod.POST, request, response);
        }

        /// <summary>
        /// Handles PUT requests
        /// </summary>
        /// <param name="request">The request</param>
        /// <param name="response">The response</param>
        /// <param name="user">The detected users identity</param>
        public void Put(System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response, System.Security.Principal.IPrincipal user)
        {
            Handle(HttpMethod.PUT, request, response);
        }

        /// <summary>
        /// Handles DELETE requests
        /// </summary>
        /// <param name="request">The request</param>
        /// <param name="response">The response</param>
        /// <param name="user">The detected users identity</param>
        public void Delete(System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response, System.Security.Principal.IPrincipal user)
        {
            Handle(HttpMethod.DELETE, request, response);
        }

        private void Handle(HttpMethod methodType, System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response)
        {
            String name = request.Url.AbsolutePath.ToUpperInvariant();
            name = name.Substring(name.LastIndexOf('/') + 1);

            Dictionary<String, IList<RestFunctionInfo>> functions;
            if (!subhandlers.TryGetValue(methodType, out functions))
            {
                response.StatusCode = 404;
                using (StreamWriter sw = new StreamWriter(response.OutputStream))
                { sw.WriteLine("There is no " + methodType.ToString() + " handler with the name: " + name); }
                log.Trace("404 - There is no " + methodType.ToString() + " handler with the name: " + name);
                return;
            }
            IList<RestFunctionInfo> overloads;
            if (!functions.TryGetValue(name, out overloads))
            {
                response.StatusCode = 404;
                using (StreamWriter sw = new StreamWriter(response.OutputStream))
                { sw.WriteLine("There is no " + methodType.ToString() + " handler with the name: " + name); }
                log.Trace("404 - There is no " + methodType.ToString() + " handler with the name: " + name);
                return;
            }
            Dictionary<String, String> args = ExtractParams(request.Url);
            HashSet<String> argNames = new HashSet<string>(args.Keys);
            Dictionary<int, RestFunctionInfo> matches = new Dictionary<int, RestFunctionInfo>();
            int bestMatch = -1;
            foreach (RestFunctionInfo overload in overloads)
            {
                int match = RankMatching(argNames, overload.ArgumentNames);
                bestMatch = Math.Max(bestMatch, match);
                if (!matches.ContainsKey(match))
                { matches.Add(match, overload); }
            }
            if (bestMatch > -1)
            {
                Handle(matches[bestMatch], args, response);
            }
            else
            {
                response.StatusCode = 404;
                using (StreamWriter sw = new StreamWriter(response.OutputStream))
                { sw.WriteLine("Arguments don't match any handlers"); }
                log.Trace("404 - Arguments don't match any handlers: " + request.Url);
            }
        }
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        private void Handle(RestFunctionInfo function, Dictionary<String, String> arguments, System.Net.HttpListenerResponse response)
        {
            MethodInfo mi = function.MethodInfo;
            ParameterInfo[] paramsInOrder = mi.GetParameters();
            Object[] typedArguments = new object[paramsInOrder.Length];
            try
            {                
                for (int i = 0; i < paramsInOrder.Length; i++)
                {
                    ParameterInfo pi = paramsInOrder[i];
                    String stringValue = null;
                    if (!arguments.TryGetValue(pi.Name.ToUpperInvariant(), out stringValue))
                    {
                        typedArguments[i] = GetDefaultValue(pi);
                    }
                    else
                    {
                        typedArguments[i] = JsonConvert.DeserializeObject(stringValue, pi.ParameterType);
                    }
                }
            }
            catch (Exception e)
            {
                response.StatusCode = 400;
                using (StreamWriter sw = new StreamWriter(response.OutputStream))
                { sw.Write(e.Message); }
                log.Trace("400 - Error parsing input: " + e.Message);
            }
            ////////////////////// Caching interupts the regular workflow
            String CacheKey = null;
            if (function.Cachable)
            {
                CacheKey = "{'f':'" + function.MethodInfo.Name + "','arg':" + JsonConvert.SerializeObject(typedArguments) + ",'hc':" + function.GetHashCode() + "}";
                //track the popular queries so they stay fresh in the cache and no one needs to wait
                _popularQueries.VoteUp(CacheKey, new QuerySnapshot() { mi = function.MethodInfo, instance = function.Instance, Args = typedArguments });
                CachedResult<String> result = null;
                if (_cachedResults.TryGetValue(CacheKey, out result))
                {
                    if (DateTime.Now.Subtract(result.TimeStamp) < function.Expiry)
                    {
                        response.StatusCode = 200;
                        response.ContentType = "application/json";
                        response.Headers["Cache-Control"] = "public, max-age=" + function.Expiry.TotalSeconds;
                        response.Headers["Expires"] = result.TimeStamp.Add(function.Expiry).ToLongDateTimeString();
                        using (StreamWriter sw = new StreamWriter(response.OutputStream))
                        { sw.Write(result.Value); }
                        return;
                    }
                }
            }
            //////////////////////////////////////////////////////////////////////
            Object returnValue;
            try
            {
                returnValue = mi.Invoke(function.Instance, typedArguments);                
            }
            catch (Exception e2)
            {
                response.StatusCode = 500;
                using (StreamWriter sw = new StreamWriter(response.OutputStream))
                { sw.Write(e2.InnerException != null ? e2.InnerException.Message : e2.Message); }
                log.Error("Error while processing request", e2.InnerException ?? e2);
                return;
            }
            response.StatusCode = 200;
            if (mi.ReturnType != null)
            {
                try
                {
                    String result = JsonConvert.SerializeObject(returnValue, Formatting.None, new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore });
                    if (function.Cachable)
                    {
                        _cachedResults[CacheKey] = new CachedResult<string>(result) ;
                        response.Headers["Cache-Control"] = "public, max-age=" + function.Expiry.TotalSeconds;
                        response.Headers["Expires"] = (new DateTime()).Add(function.Expiry).ToLongDateTimeString();
                    }
                    response.ContentType = "application/json";
                    using (StreamWriter sw = new StreamWriter(response.OutputStream))
                    { sw.Write(result); }
                }
                catch (Exception e3)
                {
                    response.StatusCode = 500;
                    using (StreamWriter sw = new StreamWriter(response.OutputStream))
                    { sw.Write("Error while serializing result: " + e3.Message); }
                    log.Error("Error while serializing response", e3);
                }
            }
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        private void RefreshPopularQueries(object ignored)
        {
            int numToPrepopulate = (int) Math.Max(_cachedResults.Count * .1, 10);
            var popular = _popularQueries.MostPopular(numToPrepopulate);
            _popularQueries.PruneData(numToPrepopulate);
            foreach (var query in popular)
            {
                try
                {
                    log.Debug("pre-fetching updated value for popular query: " + query.Value);
                    Object returnValue = query.State.mi.Invoke(query.State.instance, query.State.Args);
                    String result = JsonConvert.SerializeObject(returnValue, Formatting.None, new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore });
                    _cachedResults[query.Value] = new CachedResult<string>(result);
                }
                catch { }
            }            
        }
        
        private static int RankMatching(HashSet<String> super, HashSet<String> subset)
        {
            Contract.Requires(super != null);
            Contract.Requires(subset != null);
            if (!super.IsSupersetOf(subset))
            { 
                //not a match
                return -1; 
            }
            if (super.Count == subset.Count)
            {
                //perfect match
                return 10;
            }
            //its a match, but not the best match
            return Math.Max(0, 10 - (super.Count - subset.Count));
        }
        private static Dictionary<String, String> ExtractParams(Uri url)
        {
            Dictionary<String, String> args = new Dictionary<string, String>();
            foreach (String kvpair in url.Query.TrimStart('?').Split('&'))
            {
                String[] tokens = kvpair.Split('=');
                if (tokens.Length != 2)
                { continue; }
                args.Add(tokens[0].ToUpperInvariant(), Uri.UnescapeDataString(tokens[1]));
            }
            return args;
        }

        #region IDisposable Members
        /// <summary>
        /// Disposes the query cache timer
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        /// <summary>
        /// Disposes the query cache timer
        /// </summary>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "all")]
        protected virtual void Dispose(bool all)
        {
            if (_popularQueryUpdater != null)
            {
                _popularQueryUpdater.Dispose();
                _popularQueryUpdater = null;
            }
            if (_cachedResults != null)
            {
                _cachedResults.Dispose();
                _cachedResults = null;
            }
        }

        #endregion
    }
}
